前言
自 iOS 9 以后,Apple 加入了悬浮窗调试工具,也就是 UIDebuggingInformationOverlay 。利用它我们可以做到很多事情,例如:查看视图层级,控制器层级,页面中的变量,测量等等。那么我们如何开启这个调试工具呢?只需要添加如下的代码:
|
|
这段代码的实际意义可以转换成以下两行代码:
|
|
调用成功后,我们就能看到悬浮窗的庐山真面目了。

然而,在 iOS 11 后,上面的代码就不再 work 了。获取信息后得知是 Apple 添加了验证机制,想以此来确保只有内部的 App 链接到 UIKit 后才可以访问这些私有类。在 iOS 11之后,我们还有办法去使用这种调试工具吗?
内部实现探究
利用 LLDB 尝试去逆向 -[UIDebuggingInformationOverlay init] ,发现在 iOS 10 下,该方法的实现大致如下:
|
|
使用同样的方法去逆向 iOS 11 下的 -[UIDebuggingInformationOverlay init] ,该方法的实现大致是这样的:
|
|
从如上代码中我们可以看出,Apple 使用 UIDebuggingOverlayIsEnabled() 去验证当前设备是否是内部设备,所以,当我们去调用 [UIDebuggingInformationOverlay new] 时,会返回 nil 。
如何绕过验证机制
Serek Selander 在 Swizzling in iOS 11 with UIDebuggingInformationOverlay 文章中给出了他的方案。他的原理是通过 LLDB 找出 dispatch_once 部分代码在内存地址中的范围。dispatch_once 内部实现上是这样一个流程用 onceToken 的地址与 -1 进行比较,如果包含 -1 ,就表示已经执行过 block 中的代码,不再执行,若不包含 -1 , 则会去执行 block 中的代码,并将 onceToken 的地址置为 -1(默认初始化为 0)。他的做法就是找到 mainHandler.onceToken 的内存地址,然后将 -1 写入到该内存地址中。完整的代码如下:
|
|
改进
由于模拟器和真机的架构不同, Serek Selander 给出的代码只能在模拟器下 work ,因而为了能在真机下 work , 我做出了一些改进。完整代码如下:
|
|
主要思想是 UIDebuggingInformationOverlay 是 UIWindow 的子类,那么我们可以利用 runtime 机制动态去替换其 init 方法的 IMP,以此来绕过 Apple 的验证机制。在实例化一个 UIDebuggingInformationOverlay 对象后,调用 [[UIDebuggingInformationOverlayInvokeGestureHandler mainHandler] _handleActivationGesture:(UIGestureRecognizer *)] 触发 UIDebuggingInformationOverlay 显示到屏幕上。由于该方法要求必须要有 UIGestureRecognizer 手势,以便检查其 state 值,因而我们需要添加 state 变量用来伪装手势。最后,在实际调用的地方调用如下代码即可 work :
|
|